vimfandomcom-20200223-history
Smart mapping for tab completion
I'm used to complete words with the Tab key, however when editing source I can't just map that to Vim keyword completion because I sometime need to insert real tabs, since it mostly happen when at the beginning of the line or after a ; and before a one line comma (java, c++ or perl anyone) I've come to find the following really useful. This is how you can map the Tab key in insert mode while still being able to use it when at the start of a line or when the preceding char is not a keyword character. in a script file in a plugin directory or in your .vimrc file: first define a function which returns a Tab or the appropriate completion, depending on the context: function! Smart_TabComplete() let line = getline('.') " current line let substr = strpart(line, -1, col('.')+1) " from the start of the current " line to one character right " of the cursor let substr = matchstr(substr, "\t*$") " word till cursor if (strlen(substr) 0) " nothing to match on empty string return "\" endif let has_period = match(substr, '\.') != -1 " position of period, if any let has_slash = match(substr, '\/') != -1 " position of slash, if any if (!has_period && !has_slash) return "\\" " existing text matching elseif ( has_slash ) return "\\" " file matching else return "\\" " plugin matching endif endfunction Then define the appropriate mappings: inoremap =Smart_TabComplete() The trick here is the use of the = in insert mode to be able to call your function without leaving insert mode. See . Comments *Clean up! This is a nice tip... *Use an mapping instead of = ---- See also . It is a complete system, and the completion remembers the completion mode! ---- One behavior of the tab completion script above may be undesirable for some users (a bug, or a use case I don't understand?): it checks up to the character right of the cursor, so that it may "complete" a blank string. For example, placing the cursor to the left of "foo" in the following line: " foo ... and pressing Tab would attempt to "complete" a word, instead of inserting a tab. If this behavior is undesirable, simply remove the "+1" from "col('.')+1" in the script above. ---- for those of us that would like to use dictionary files for filetypes ( in order to use function prototypes et. al.) you can modify the 'iskeyword' option temporarily to complete function names for example : given a dictionary with abs(VALUE) I could type : ab and it would expand to abs(VALUE) function! InsertTabWrapper(direction) let oldisk=&isk "save the iskeyword options set isk+=(,),, "add '(' ')' and ',' character let col = col('.') - 1 if !col || getline('.')- 1 !~ '\k' return "\" elseif "backward" a:direction return "\" else return "\" endif set &isk=oldisk "restore the iskeyword options endfunction fun! Iskcompletion() let oldisk=&isk set isk+=(,) normal set &isk=oldisk endfun If you want to use keywords in completion, you'll need to make dictionary files for each of the languages whose keywords you want to use. e.g. grep keyword /usr/share/vim/vim61/syntax/sh.vim | grep -v nextgroup | awk '{ $1=""; $2=""; $3=""; print}' | perl -pe 's/\s+/\n/g' | grep -v contained | grep -v '^$' | sort | uniq > /home/user/.vim/dict/sh.dict the above command will probably need tweaking depending on the syntax file. and in your .vimrc: " tell complete to look in the dictionary set complete-=k complete+=k " load the dictionary according to syntax :au BufReadPost * if exists("b:current_syntax") :au BufReadPost * let &dictionary = substitute("~/.vim/dict/FT.dict", "FT", b:current_syntax, "") :au BufReadPost * endif I prefer to be able to toggle tab completion manually. The following function works for me: " toggle tab completion function! TabCompletion() if mapcheck("\", "i") != "" :iunmap :iunmap :iunmap echo "tab completion off" else :imap :imap :imap echo "tab completion on" endif endfunction map tc :call TabCompletion() ---- Have to chime in... here's my silly attempt at making the Tab key more intelligent. The function below (derived work of course) allows one to get the original effect of if the previous character is a space. I.e., you can type: foo and it'll end up as foo with the space deleted. This is *not* the same as , which would always insert a real Tab character rather than honoring 'softtabstop'. " Intelligent tab completion inoremap =InsertTabWrapper(1) inoremap =InsertTabWrapper(-1) function! InsertTabWrapper(direction) let idx = col('.') - 1 let str = getline('.') if a:direction > 0 && idx >= 2 && str- 1 ' ' \&& str- 2 =~? 'a-z' if &softtabstop && idx % &softtabstop 0 return "\\\" else return "\\" endif elseif idx 0 || str- 1 !~? 'a-z' return "\" elseif a:direction > 0 return "\" else return "\" endif endfunction What about this to dump all the syntax files into properly formatted dict files for i in /usr/share/vim/syntax/*;do grep keyword $i | grep -v nextgroup | awk '{ $1=""; $2=""; $3=""; print}' | perl -pe 's/\s+/\n/g' | grep -v contained | grep -v '^$' | sort | uniq>~/.vim/dict/`basename $i .vim`.dict;done One small suggestion for daniel elstner's script: replace 'a-z' with '^'. ---- My derivation of this wonderful tip: (aimed at C++ // comments) This as it happens has nothing to do with Tab nor completion. function! SpecialCR() " Feral This is inspired by how multi-edit does things. let DaLine = getline('.') if match(DaLine, '\c^\s*//$') > -1 " Feral:213/03@04:59 Just eat the // chars " return "\\" " "This method: " // -Feral:213/03@04:59---------------------------------------------- " // " 75 is the column we wish to not go beyond. " 2 = ' -' " 20 = my time stamp: Feral:213/03@04:52 " 1 = '-' let AmountForFiller = 75 - (virtcol('.')+2+20+1) let Filler = "" while strlen(Filler) < AmountForFiller let Filler = Filler.'-' endwhile let DaLine = " -Feral:".strftime('%j/%y@%H:%M')."-".Filler."\" return DaLine elseif match(DaLine, '\c^\s*//\s$') > -1 return "\\\" else return "\" endif endfunction inoremap =SpecialCR() This will make eat C++ line comments when they are the only think on the line, or more precisely: "//\s" will be get 3 backspaces which should erase it and "//" will get a simple timestamp separator. ---- Here's the section of my vimrc that has all this wired up (this is on a windows box, I jumped on a unix machine to run the shell script that makes all the dict files). My addition is Ctrl-TAB to begin a keyword search instead of hitting Ctrl-X Ctrl-K, then I can regular TAB through the entries. Watch out for those dang spaces at the end of the lines! Thanks to everybody who contributed, this is great. " Remap TAB to keyword completion function! InsertTabWrapper(direction) let col = col('.') - 1 if !col || getline('.')- 1 !~ '\k' return "\" elseif "backward" a:direction return "\" elseif "forward" a:direction return "\" else return "\\" endif endfunction inoremap =InsertTabWrapper ("forward") inoremap =InsertTabWrapper ("backward") inoremap =InsertTabWrapper ("startkey") " toggle tab completion function! ToggleTabCompletion() if mapcheck("\", "i") != "" :iunmap :iunmap :iunmap echo "tab completion off" else :imap :imap :imap echo "tab completion on" endif endfunction map tc :call ToggleTabCompletion() " tell complete to look in the dictionary set complete-=k complete+=k " load the dictionary according to syntax :au BufReadPost * if exists("b:current_syntax") :au BufReadPost * let &dictionary = substitute("C:\\vim\\vimfiles\\dict\\FT.dict", "FT", b:current_syntax, "") :au BufReadPost * endif This script doesn't work with multibyte (utf-8 chars) ---- Try replace condition with !col || strpart(getline('.'), col-1, col) =~ '\s' ---- your tab-completion tip is very nice. Although my kde-konsole didn't recognize the shift-tab, combination. I solved this by adding the following line to /usr/share/apps/konsole/linux.keytab key Backtab : "\E[Z" and set the keyboard settings in konsole to "linux console". ---- In the following bit, it's more useful if you substituted "BufEnter" for "BufReadPost". At least, for those who switch between Vim windows of differing file types (for example I go between perl and cpp and bash quit a bit) >" load the dictionary according to syntax >:au BufReadPost * if exists("b:current_syntax") >:au BufReadPost * let &dictionary = substitute("C:\\vim\\vimfiles\\dict\\FT.dict", "FT", b:current_syntax, "") >:au BufReadPost * endif In you can find following settings. function! CleverTab() if strpart( getline('.'), 0, col('.')-1 ) =~ '^\s*$' return "\" else return "\" endfunction inoremap =CleverTab() It works fine, but this never uses "omni-completion" introduced from Vim 7. To use omni-completion if available, then it should be followings. function! CleverTab() if pumvisible() return "\" endif if strpart( getline('.'), 0, col('.')-1 ) =~ '^\s*$' return "\" elseif exists('&omnifunc') && &omnifunc != '' return "\\" else return "\" endif endfunction inoremap =CleverTab() ---- Avoid flicker by using an expr mapping All of the above techniques using =... cause flicker in my xterm, caused by 3 or 4 full screen redraws. This can be very slow when dealing with a large syntax-rich screen. One of the TODOs recommends using an instead, and this worked perfectly for me, avoiding all the redraws! I simply changed: inoremap =InsertTabWrapper ("forward") into: inoremap InsertTabWrapper("forward") I can highly recommend this approach. If you agree, perhaps we should update the scripts on this page. However there is one disadvantage: It may be easier for other plugins to replay a =... mapping than an mapping. (For example, modified versions of UltiSnips which fall back to the original mapping when UltiSnips has no snippet to complete.)